app.js ➔ Widgets   B
last analyzed

Complexity

Conditions 1
Paths 8

Size

Total Lines 129

Duplication

Lines 0
Ratio 0 %

Importance

Changes 8
Bugs 0 Features 0
Metric Value
cc 1
dl 0
loc 129
rs 7
c 8
b 0
f 0
nc 8
nop 0

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
/** global: d3 */
2
/**
3
 * Bootstrapping functions, event handling, etc... for application.
4
 */
5
6
var jsondash = function() {
7
    var my = {
8
        chart_wall: null
9
    };
10
    var MIN_CHART_SIZE   = 200;
11
    var API_ROUTE_URL    = $('[name="dataSource"]');
12
    var API_PREVIEW      = $('#api-output');
13
    var API_PREVIEW_BTN  = $('#api-output-preview');
14
    var API_PREVIEW_CONT = $('.api-preview-container');
15
    var WIDGET_FORM      = $('#module-form');
16
    var VIEW_BUILDER     = $('#view-builder');
17
    var ADD_MODULE       = $('#add-module');
18
    var MAIN_CONTAINER   = $('#container');
19
    var EDIT_MODAL       = $('#chart-options');
20
    var DELETE_BTN       = $('#delete-widget');
21
    var DELETE_DASHBOARD = $('.delete-dashboard');
22
    var SAVE_WIDGET_BTN  = $('#save-module');
23
    var EDIT_CONTAINER   = $('#edit-view-container');
24
    var MAIN_FORM        = $('#save-view-form');
25
    var JSON_DATA        = $('#raw-config');
26
    var ADD_ROW_CONTS    = $('.add-new-row-container');
27
    var EDIT_TOGGLE_BTN  = $('[href=".edit-mode-component"]');
28
    var UPDATE_FORM_BTN  = $('#update-module');
29
    var CHART_TEMPLATE   = $('#chart-template');
30
    var ROW_TEMPLATE     = $('#row-template').find('.grid-row');
31
    var EVENTS           = {
32
        init:             'jsondash.init',
33
        edit_form_loaded: 'jsondash.editform.loaded',
34
        add_widget:       'jsondash.widget.added',
35
        update_widget:    'jsondash.widget.updated',
36
        delete_widget:    'jsondash.widget.deleted',
37
        refresh_widget:   'jsondash.widget.refresh',
38
        add_row:          'jsondash.row.add',
39
        delete_row:       'jsondash.row.delete',
40
        preview_api:      'jsondash.preview',
41
    }
42
43
    /**
44
     * [Widgets A singleton manager for all widgets.]
45
     */
46
    function Widgets() {
47
        var self = this;
48
        self.widgets = {};
49
        self.url_cache = {};
50
        self.container = MAIN_CONTAINER.selector;
51
        self.all = function() {
52
            return self.widgets;
53
        };
54
        self.add = function(config) {
55
            self.widgets[config.guid] = new Widget(self.container, config);
56
            self.widgets[config.guid].$el.trigger(EVENTS.add_widget);
57
            return self.widgets[config.guid];
58
        };
59
        self.addFromForm = function() {
60
            return self.add(self.newModel());
61
        };
62
        self._delete = function(guid) {
63
            delete self.widgets[guid];
64
        };
65
        self.get = function(guid) {
66
            return self.widgets[guid];
67
        };
68
        self.getByEl = function(el) {
69
            return self.get(el.data().guid);
70
        };
71
        /**
72
         * [getAllMatchingProp Get widget guids matching a givne propname and val]
73
         */
74
        self.getAllMatchingProp = function(propname, val) {
75
            var matches = [];
76
            $.each(self.all(), function(i, widg){
77
                if(widg.config[propname] === val) {
78
                    matches.push(widg.config.guid);
79
                }
80
            });
81
            return matches;
82
        };
83
        /**
84
         * [getAllOfProp Get all the widgets' config values of a specified property]
85
         */
86
        self.getAllOfProp = function(propname) {
87
            var props = [];
88
            $.each(self.all(), function(i, widg){
89
                props.push(widg.config[propname]);
90
            });
91
            return props;
92
        };
93
        self.getAllOfPropUnless = function(propname, propcheck, val) {
94
            var props = [];
95
            $.each(self.all(), function(i, widg){
96
                if(widg.config[propcheck] !== val) {
97
                    props.push(widg.config[propname]);
98
                }
99
            });
100
            return props;
101
        };
102
        /**
103
         * [loadAll Load all widgets at once in succession]
104
         */
105
        self.loadAll = function() {
106
            // Don't run this on certain types that are not cacheable (e.g. binary, html)
107
            var config_urls = self.getAllOfPropUnless('dataSource', 'family', 'Basic');
108
            var unique_urls = d3.set(config_urls).values();
109
            var cached = {};
110
            var proms = [];
111
            // Build out promises.
112
            $.each(unique_urls, function(_, url){
113
                var req = $.ajax({
114
                    url: url,
115
                    type: 'GET',
116
                    dataType: 'json',
117
                    error: function(error){
118
                        var matches = self.getAllMatchingProp('dataSource', url);
119
                        $.each(matches, function(_, guid){
120
                            var widg = self.get(guid);
121
                            jsondash.handleRes(error, null, widg.el);
122
                        });
123
                    }
124
                });
125
                proms.push(req);
126
            });
127
            // Retrieve and gather the promises
128
            $.when.apply($, proms).done(whenAllDone);
129
130
            function whenAllDone() {
131
                $.each(arguments, function(index, prom){
132
                    var ref_url = unique_urls[index];
133
                    var data = null;
0 ignored issues
show
Unused Code introduced by
The assignment to data seems to be never used. If you intend to free memory here, this is not necessary since the variable leaves the scope anyway.
Loading history...
134
                    if(ref_url) {
135
                        data = prom[0];
136
                        cached[ref_url] = data;
137
                    }
138
                });
139
                // Inject a cached value on the config for use down the road
140
                // (this is done so little is changed with the architecture of getting and loading).
141
                for(var guid in self.all()){
142
                    // Don't refresh, just update config with new key value for cached data.
143
                    var widg = self.get(guid);
144
                    var data = cached[widg.config.dataSource];
145
                    // Grab data from specific `key` key, if it exists (for shared data on a single endpoint).
146
                    var cachedData = widg.config.key && data.multicharts ? data.multicharts[widg.config.key] : data;
147
                    widg.update({cachedData: cachedData}, true);
148
                    // Actually load them all
149
                    widg.load();
150
                }
151
            }
152
        };
153
        self.newModel = function() {
154
            var config = getParsedFormConfig();
155
            var guid   = jsondash.util.guid();
156
            config['guid'] = guid;
157
            if(!config.refresh || !refreshableType(config.type)) {
158
                config['refresh'] = false;
159
            }
160
            return config;
161
        };
162
        self.populate = function(data) {
163
            for(var name in data.modules){
0 ignored issues
show
Complexity introduced by
A for in loop automatically includes the property of any prototype object, consider checking the key using hasOwnProperty.

When iterating over the keys of an object, this includes not only the keys of the object, but also keys contained in the prototype of that object. It is generally a best practice to check for these keys specifically:

var someObject;
for (var key in someObject) {
    if ( ! someObject.hasOwnProperty(key)) {
        continue; // Skip keys from the prototype.
    }

    doSomethingWith(key);
}
Loading history...
164
                // Closure to maintain each chart data value in loop
165
                (function(config){
166
                    var config = data.modules[name];
0 ignored issues
show
introduced by
The variable name is changed by the for-each loop on line 163. Only the value of the last iteration will be visible in this function if it is called outside of the loop.
Loading history...
167
                    // Add div wrappers for js grid layout library,
168
                    // and add title, icons, and buttons
169
                    // This is the widget "model"/object used throughout.
170
                    self.add(config);
171
                })(data.modules[name]);
172
            }
173
        };
174
    }
175
176
    function Widget(container, config) {
177
        // model for a chart widget
178
        var self = this;
179
        self.config = config;
180
        self.guid = self.config.guid;
181
        self.container = container;
182
        self._refreshInterval = null;
183
        self._makeWidget = function(config) {
184
            if(document.querySelector('[data-guid="' + config.guid + '"]')){
185
                return d3.select('[data-guid="' + config.guid + '"]');
186
            }
187
            return d3.select(self.container).select('div')
188
                .append('div')
189
                .classed({item: true, widget: true})
190
                .attr('data-guid', config.guid)
191
                .attr('data-refresh', config.refresh)
192
                .attr('data-refresh-interval', config.refreshInterval)
193
                .style('width', config.width + 'px')
194
                .style('height', config.height + 'px')
195
                .html(d3.select(CHART_TEMPLATE.selector).html())
196
                .select('.widget-title .widget-title-text').text(config.name);
197
        };
198
        // d3 el
199
        self.el = self._makeWidget(config);
200
        // Jquery el
201
        self.$el = $(self.el[0]);
202
        self.init = function() {
203
            // Add event handlers for widget UI
204
            self.$el.find('.widget-refresh').on('click.charts', refreshWidget);
205
            self.$el.find('.widget-delete').on('click.charts.delete', function(e){
0 ignored issues
show
Unused Code introduced by
The parameter e is not used and could be removed.

This check looks for parameters in functions that are not used in the function body and are not followed by other parameters which are used inside the function.

Loading history...
206
                self.delete();
207
            });
208
            // Allow swapping of edit/update events
209
            // for the edit button and form modal
210
            self.$el.find('.widget-edit').on('click.charts', function(){
211
                SAVE_WIDGET_BTN
212
                .attr('id', UPDATE_FORM_BTN.selector.replace('#', ''))
213
                .text('Update widget')
214
                .off('click.charts.save')
215
                .on('click.charts', onUpdateWidget);
216
            });
217
            if(self.config.refresh && self.config.refreshInterval) {
218
                self._refreshInterval = setInterval(function(){
219
                    self.load();
220
                }, parseInt(self.config.refreshInterval, 10));
221
            }
222
            if(my.layout === 'grid') {
223
                updateRowControls();
224
            }
225
        };
226
        self.getInput = function() {
227
            // Get the form input for this widget.
228
            return $('input[id="' + self.guid + '"]');
229
        };
230
        self.delete = function(bypass_confirm) {
231
            if(!bypass_confirm){
232
                if(!confirm('Are you sure?')) {
233
                    return;
234
                }
235
            }
236
            var row = self.$el.closest('.grid-row');
237
            clearInterval(self._refreshInterval);
238
            // Delete the input
239
            self.getInput().remove();
240
            self.$el.trigger(EVENTS.delete_widget, [self]);
241
            // Delete the widget
242
            self.el.remove();
243
            // Remove reference to the collection by guid
244
            my.widgets._delete(self.guid);
245
            EDIT_MODAL.modal('hide');
246
            // Redraw wall to replace visual 'hole'
247
            if(my.layout === 'grid') {
248
                // Fill empty holes in this charts' row
249
                fillEmptyCols(row);
250
                updateRowControls();
251
            }
252
            // Trigger update form into view since data is dirty
253
            EDIT_CONTAINER.collapse('show');
254
            // Refit grid - this should be last.
255
            fitGrid();
256
        };
257
        self.addGridClasses = function(sel, classes) {
258
            d3.map(classes, function(colcount){
259
                var classlist = {};
260
                classlist['col-md-' + colcount] = true;
261
                classlist['col-lg-' + colcount] = true;
262
                sel.classed(classlist);
263
            });
264
        };
265
        self.removeGridClasses = function(sel) {
266
            var bootstrap_classes = d3.range(1, 13);
267
            d3.map(bootstrap_classes, function(i){
268
                var classes = {};
269
                classes['col-md-' + i] = false;
270
                classes['col-lg-' + i] = false;
271
                sel.classed(classes);
272
            });
273
        };
274
        self.update = function(conf, dont_refresh) {
275
                /**
276
             * Single source to update all aspects of a widget - in DOM, in model, etc...
277
             */
278
            var widget = self.el;
279
            // Update model data
280
            self.config = $.extend(self.config, conf);
281
            // Trigger update form into view since data is dirty
282
            // Update visual size to existing widget.
283
            loader(widget);
284
            widget.style({
285
                height: self.config.height + 'px',
286
                width: my.layout === 'grid' ? '100%' : self.config.width + 'px'
287
            });
288
            if(my.layout === 'grid') {
289
                // Extract col number from config: format is "col-N"
290
                var colcount = self.config.width.split('-')[1];
291
                var parent = d3.select(widget.node().parentNode);
292
                // Reset all other grid classes and then add new one.
293
                self.removeGridClasses(parent);
294
                self.addGridClasses(parent, [colcount]);
295
                // Update row buttons based on current state
296
                updateRowControls();
297
            }
298
            widget.select('.widget-title .widget-title-text').text(self.config.name);
299
            // Update the form input for this widget.
300
            self._updateForm();
301
302
            if(!dont_refresh) {
303
                self.load();
304
                EDIT_CONTAINER.collapse();
305
                // Refit the grid
306
                fitGrid();
307
            } else {
308
                unload(widget);
309
            }
310
            $(widget[0]).trigger(EVENTS.update_widget);
311
        };
312
        self.load = function() {
313
            var widg      = my.widgets.get(self.guid);
314
            var widget    = self.el;
315
            var $widget   = self.$el;
316
            var config    = widg.config;
317
            var inputs    = $widget.find('.chart-inputs');
318
            var container = $('<div></div>').addClass('chart-container');
319
            var family    = config.family.toLowerCase();
320
321
            widget.classed({error: false});
322
            widget.select('.error-overlay')
323
                .classed({hidden: true})
324
                .select('.alert')
325
                .text('');
326
327
            loader(widget);
328
329
            try {
330
                // Cleanup for all widgets.
331
                widget.selectAll('.chart-container').remove();
332
                // Ensure the chart inputs comes AFTER any chart container.
333
                if(inputs.length > 0) {
334
                    inputs.before(container);
335
                } else {
336
                    $widget.append(container);
337
                }
338
                // Handle any custom inputs the user specified for this module.
339
                // They map to standard form inputs and correspond to query
340
                // arguments for this dataSource.
341
                if(config.inputs) {
342
                    handleInputs(widg, config);
343
                }
344
345
                // Retrieve and immediately call the appropriate handler.
346
                getHandler(family)(widget, config);
347
348
            } catch(e) {
349
                if(console && console.error) console.error(e);
0 ignored issues
show
Coding Style Best Practice introduced by
Curly braces around statements make for more readable code and help prevent bugs when you add further statements.

Consider adding curly braces around all statements when they are executed conditionally. This is optional if there is only one statement, but leaving them out can lead to unexpected behaviour if another statement is added later.

Consider:

if (a > 0)
    b = 42;

If you or someone else later decides to put another statement in, only the first statement will be executed.

if (a > 0)
    console.log("a > 0");
    b = 42;

In this case the statement b = 42 will always be executed, while the logging statement will be executed conditionally.

if (a > 0) {
    console.log("a > 0");
    b = 42;
}

ensures that the proper code will be executed conditionally no matter how many statements are added or removed.

Loading history...
350
                widget.classed({error: true});
351
                widget.select('.error-overlay')
352
                    .classed({hidden: false})
353
                    .select('.alert')
354
                    .text('Loading error: "' + e + '"');
355
                unload(widget);
356
            }
357
            addResizeEvent(widg);
358
        };
359
        self._updateForm = function() {
360
            self.getInput().val(JSON.stringify(self.config));
361
        };
362
363
        // Run init script on creation
364
        self.init();
365
    }
366
367
    /**
368
     * [fillEmptyCols Fill in gaps in a row when an item has been deleted (fixed grid only)]
369
     */
370
    function fillEmptyCols(row) {
371
        row.each(function(_, row){
372
            var items = $(row).find('.item.widget');
0 ignored issues
show
Unused Code introduced by
The variable items seems to be never used. Consider removing it.
Loading history...
373
            var cols = $(row).find('> div');
374
            cols.filter(function(i, col){
375
                return $(col).find('.item.widget').length === 0;
376
            }).remove();
377
        });
378
    }
379
380
    function togglePreviewOutput(is_on) {
381
        if(is_on) {
382
            API_PREVIEW_CONT.show();
383
            return;
384
        }
385
        API_PREVIEW_CONT.hide();
386
    }
387
388
    function previewAPIRoute(e) {
389
        e.preventDefault();
390
        // Shows the response of the API field as a json payload, inline.
391
        $.ajax({
392
            type: 'GET',
393
            url: API_ROUTE_URL.val().trim(),
394
            success: function(data) {
395
                API_PREVIEW.html(prettyCode(data));
396
                API_PREVIEW.trigger(EVENTS.preview_api, [{status: data, error: false}]);
397
            },
398
            error: function(data, status, error) {
399
                API_PREVIEW.html(error);
400
                API_PREVIEW.trigger(EVENTS.preview_api, [{status: data, error: true}]);
401
            }
402
        });
403
    }
404
405
    function refreshableType(type) {
406
        if(type === 'youtube') {return false;}
407
        return true;
408
    }
409
410
    function validateWidgetForm() {
411
        var is_valid = true;
412
        var url_field = WIDGET_FORM.find('[name="dataSource"]');
413
        WIDGET_FORM.find('[required]').each(function(i, el){
414
            if($(el).val() === '') {
415
                $(el).parent().addClass('has-error').removeClass('has-success');
416
                is_valid = false;
417
                return false;
418
            } else {
419
                $(el).parent().addClass('has-success').removeClass('has-error');
0 ignored issues
show
Best Practice introduced by
There is no return statement in this branch, but you do return something in other branches. Did you maybe miss it? If you do not want to return anything, consider adding return undefined; explicitly.
Loading history...
420
            }
421
        });
422
        // Validate youtube videos
423
        if(WIDGET_FORM.find('[name="type"]').val() === 'youtube') {
424
            if(!url_field.val().startsWith('<iframe')) {
425
                url_field.parent().addClass('has-error');
426
                is_valid = false;
0 ignored issues
show
Unused Code introduced by
The assignment to variable is_valid seems to be never used. Consider removing it.
Loading history...
427
                return false;
428
            }
429
        }
430
        return is_valid;
431
    }
432
433
    function saveWidget(e){
0 ignored issues
show
Unused Code introduced by
The parameter e is not used and could be removed.

This check looks for parameters in functions that are not used in the function body and are not followed by other parameters which are used inside the function.

Loading history...
434
        if(!(validateWidgetForm())) {
435
            return false;
436
        }
437
        var new_config = my.widgets.newModel();
438
        // Remove empty rows and then update the order so it's consecutive.
439
        $('.grid-row').not('.grid-row-template').each(function(i, row){
440
            // Delete empty rows - except any empty rows that have been created
441
            // for the purpose of this new chart.
442
            if($(row).find('.item.widget').length === 0 && new_config.row !== i + 1) {
443
                $(row).remove();
444
            }
445
        });
446
        // Update the row orders after deleting empty ones
447
        updateRowOrder();
448
        var newfield = $('<input class="form-control" type="text">');
449
        // Add a unique guid for referencing later.
450
        newfield.attr('name', 'module_' + new_config.id);
451
        newfield.val(JSON.stringify(new_config));
452
        $('.modules').append(newfield);
453
        // Save immediately.
454
        MAIN_FORM.submit();
0 ignored issues
show
Best Practice introduced by
There is no return statement in this branch, but you do return something in other branches. Did you maybe miss it? If you do not want to return anything, consider adding return undefined; explicitly.
Loading history...
455
    }
456
457
    function isModalButton(e) {
458
        return e.relatedTarget.id === ADD_MODULE.selector.replace('#', '');
459
    }
460
461
    function isRowButton(e) {
462
        return $(e.relatedTarget).hasClass('grid-row-label');
463
    }
464
465
    function clearForm() {
466
        WIDGET_FORM.find('label')
467
        .removeClass('has-error')
468
        .removeClass('has-success')
469
        .find('input, select')
470
        .each(function(_, input){
471
            $(input).val('');
472
        });
473
    }
474
475
    function deleteRow(row) {
476
        var rownum = row.find('.grid-row-label').data().row;
0 ignored issues
show
Unused Code introduced by
The variable rownum seems to be never used. Consider removing it.
Loading history...
477
        row.find('.item.widget').each(function(i, widget){
0 ignored issues
show
Unused Code introduced by
The parameter i is not used and could be removed.

This check looks for parameters in functions that are not used in the function body and are not followed by other parameters which are used inside the function.

Loading history...
Unused Code introduced by
The parameter widget is not used and could be removed.

This check looks for parameters in functions that are not used in the function body and are not followed by other parameters which are used inside the function.

Loading history...
478
            var guid = $(this).data().guid;
479
            var widget = my.widgets.get(guid).delete(true);
0 ignored issues
show
Unused Code introduced by
The variable widget seems to be never used. Consider removing it.
Loading history...
480
        });
481
        // Remove AFTER removing the charts contained within
482
        row.remove();
483
        updateRowOrder();
484
        el.trigger(EVENTS.delete_row);
0 ignored issues
show
Bug introduced by
The variable el seems to be never declared. If this is a global, consider adding a /** global: el */ comment.

This checks looks for references to variables that have not been declared. This is most likey a typographical error or a variable has been renamed.

To learn more about declaring variables in Javascript, see the MDN.

Loading history...
485
    }
486
487
    function populateEditForm(e) {
488
        // If the modal caller was the add modal button, skip populating the field.
489
        API_PREVIEW.text('...');
490
        clearForm();
491
        if(isModalButton(e) || isRowButton(e)) {
492
            DELETE_BTN.hide();
493
            if(isRowButton(e)) {
494
                var row = $(e.relatedTarget).data().row;
495
                populateRowField(row);
496
                // Trigger the order field update based on the current row
497
                WIDGET_FORM.find('[name="row"]').change();
498
            } else {
499
                populateRowField();
500
            }
501
            return;
502
        }
503
        DELETE_BTN.show();
504
        // Updates the fields in the edit form to the active widgets values.
505
        var item = $(e.relatedTarget).closest('.item.widget');
506
        var guid = item.data().guid;
507
        var widget = my.widgets.get(guid);
508
        var conf = widget.config;
509
        populateRowField(conf.row);
510
        // Update the modal fields with this widgets' value.
511
        $.each(conf, function(field, val){
512
            if(field === 'override' || field === 'refresh') {
513
                WIDGET_FORM.find('[name="' + field + '"]').prop('checked', val);
514
            } else if(field === 'classes') {
515
                WIDGET_FORM.find('[name="' + field + '"]').val(val.join(','));
516
            } else {
517
                WIDGET_FORM.find('[name="' + field + '"]').val(val);
518
            }
519
        });
520
        // Update with current guid for referencing the module.
521
        WIDGET_FORM.attr('data-guid', guid);
522
        // Populate visual GUID
523
        $('[data-view-chart-guid]').find('.guid-text').text(guid);
524
        populateOrderField(widget);
525
        // Update form for specific row if row button was caller
526
        // Trigger event for select dropdown to ensure any UI is consistent.
527
        // This is done AFTER the fields have been pre-populated.
528
        WIDGET_FORM.find('[name="type"]').change();
529
        // A trigger for 3rd-party/external js to use to listen to.
530
        WIDGET_FORM.trigger(EVENTS.edit_form_loaded);
531
    }
532
533
    function populateRowField(row) {
534
        var rows_field = $('[name="row"]');
535
        var num_rows = $('.grid-row').not('.grid-row-template').length;
536
        // Don't try and populate if not in freeform mode.
537
        if(my.layout === 'freeform') {return;}
538
        if(num_rows === 0){
539
            addNewRow();
540
        }
541
        rows_field.find('option').remove();
542
        // Add new option fields - d3 range is exclusive so we add one
543
        d3.map(d3.range(1, num_rows + 1), function(i){
544
            var option = $('<option></option>');
545
            option.val(i).text('row ' + i);
546
            rows_field.append(option);
547
        });
548
        // Update current value
549
        if(row) {rows_field.val(row)};
550
    }
551
552
    /**
553
     * [populateOrderField Destroy and re-create order dropdown input based on number of items in a row, or in a dashboard.]
554
     * @param  {[object]} config [The widget config (optional)]
0 ignored issues
show
Documentation introduced by
The parameter config does not exist. Did you maybe forget to remove this comment?
Loading history...
555
     */
556
    function populateOrderField(widget) {
557
        // Add the number of items to order field.
558
        var order_field = WIDGET_FORM.find('[name="order"]');
559
        var max_options = 0;
560
        if(my.layout === 'grid') {
561
            if(!widget) {
562
                var row = WIDGET_FORM.find('[name="row"]').val();
563
                // Get the max options based on the currently selected value in the row dropdown
564
                // We also add one since this is "adding" a new item so the order should include
565
                // one more than is currently there.
566
                max_options = $('.grid-row').eq(row - 1).find('.item.widget').length + 1;
567
            } else {
568
                // Get parent row and find number of widget children for this rows' order max
569
                max_options = $(widget.el[0]).closest('.grid-row').find('.item.widget').length;
570
            }
571
        } else {
572
            var widgets = $('.item.widget');
573
            max_options = widgets.length > 0 ? widgets.length: 2;
574
        }
575
        order_field.find('option').remove();
576
        // Add empty option.
577
        order_field.append('<option value=""></option>');
578
        d3.map(d3.range(1, max_options + 1), function(i){
579
            var option = $('<option></option>');
580
            option.val(i).text(i);
581
            order_field.append(option);
582
        });
583
        order_field.val(widget && widget.config ? widget.config.order : '');
584
    }
585
586
    /**
587
     * [getParsedFormConfig Get a config usable for each json widget based on the forms active values.]
588
     * @return {[object]} [The serialized config]
589
     */
590
    function getParsedFormConfig() {
591
        function parseNum(num) {
592
            // Like parseInt, but always returns a Number.
593
            if(isNaN(parseInt(num, 10))) {
594
                return 0;
595
            }
596
            return parseInt(num, 10);
597
        }
598
        var form = WIDGET_FORM;
599
        var conf = {
600
            name: form.find('[name="name"]').val(),
601
            type: form.find('[name="type"]').val(),
602
            family: form.find('[name="type"]').find('option:checked').data() ? form.find('[name="type"]').find('option:checked').data().family : null,
603
            width: form.find('[name="width"]').val(),
604
            height: parseNum(form.find('[name="height"]').val(), 10),
0 ignored issues
show
Bug introduced by
The call to parseNum seems to have too many arguments starting with 10.
Loading history...
605
            dataSource: form.find('[name="dataSource"]').val(),
606
            override: form.find('[name="override"]').is(':checked'),
607
            order: parseNum(form.find('[name="order"]').val(), 10),
608
            refresh: form.find('[name="refresh"]').is(':checked'),
609
            refreshInterval: jsondash.util.intervalStrToMS(form.find('[name="refreshInterval"]').val()),
610
            classes: getClasses(form)
611
        };
612
        if(my.layout === 'grid') {
613
            conf['row'] = parseNum(form.find('[name="row"]').val());
614
        }
615
        return conf;
616
    }
617
618
    function getClasses(form) {
619
        var classes = form.find('[name="classes"]').val().replace(/\ /gi, '').split(',');
620
        return classes.filter(function(el, i){
0 ignored issues
show
Unused Code introduced by
The parameter i is not used and could be removed.

This check looks for parameters in functions that are not used in the function body and are not followed by other parameters which are used inside the function.

Loading history...
621
            return el !== '';
622
        });
623
    }
624
625
    function onUpdateWidget(e){
0 ignored issues
show
Unused Code introduced by
The parameter e is not used and could be removed.

This check looks for parameters in functions that are not used in the function body and are not followed by other parameters which are used inside the function.

Loading history...
626
        var guid = WIDGET_FORM.attr('data-guid');
627
        var widget = my.widgets.get(guid);
628
        var conf = getParsedFormConfig();
629
        widget.update(conf);
630
    }
631
632
    function refreshWidget(e) {
633
        e.preventDefault();
634
        var el = my.widgets.getByEl($(this).closest('.widget'));
635
        el.$el.trigger(EVENTS.refresh_widget);
636
        el.load();
637
        fitGrid();
638
    }
639
640
    /**
641
     * [isPreviewableType Determine if a chart type can be previewed in the 'preview api' section of the modal]
642
     * @param  {[type]}  string [The chart type]
0 ignored issues
show
Documentation introduced by
The parameter string does not exist. Did you maybe forget to remove this comment?
Loading history...
643
     * @return {Boolean}      [Whether or not it's previewable]
644
     */
645
    function isPreviewableType(type) {
646
        if(type === 'iframe') {return false;}
647
        if(type === 'youtube') {return false;}
648
        if(type === 'custom') {return false;}
649
        if(type === 'image') {return false;}
650
        return true;
651
    }
652
653
    /**
654
     * [chartsTypeChanged Event handler for onChange event for chart type field]
655
     */
656
    function chartsTypeChanged(e) {
0 ignored issues
show
Unused Code introduced by
The parameter e is not used and could be removed.

This check looks for parameters in functions that are not used in the function body and are not followed by other parameters which are used inside the function.

Loading history...
657
        var active_conf = getParsedFormConfig();
658
        var previewable = isPreviewableType(active_conf.type);
659
        togglePreviewOutput(previewable);
660
    }
661
662
    function populateGridWidthDropdown() {
663
        var cols = d3.range(1, 13).map(function(i, v){return 'col-' + i;});;
0 ignored issues
show
Unused Code introduced by
The parameter v is not used and could be removed.

This check looks for parameters in functions that are not used in the function body and are not followed by other parameters which are used inside the function.

Loading history...
664
        var form = d3.select(WIDGET_FORM.selector);
665
        form.select('[name="width"]').remove();
666
        form
667
            .append('select')
668
            .attr('name', 'width')
669
            .selectAll('option')
670
            .data(cols)
671
            .enter()
672
            .append('option')
673
            .value(function(i, v){
0 ignored issues
show
Unused Code introduced by
The parameter v is not used and could be removed.

This check looks for parameters in functions that are not used in the function body and are not followed by other parameters which are used inside the function.

Loading history...
674
                return i;
675
            })
676
            .text(function(i, v){
0 ignored issues
show
Unused Code introduced by
The parameter v is not used and could be removed.

This check looks for parameters in functions that are not used in the function body and are not followed by other parameters which are used inside the function.

Loading history...
677
                return i;
678
            });
679
    }
680
681
    function chartsModeChanged(e) {
0 ignored issues
show
Unused Code introduced by
The parameter e is not used and could be removed.

This check looks for parameters in functions that are not used in the function body and are not followed by other parameters which are used inside the function.

Loading history...
682
        var mode = MAIN_FORM.find('[name="mode"]').val();
683
        if(mode === 'grid') {
684
            populateGridWidthDropdown();
685
        }
686
    }
687
688
    function chartsRowChanged(e) {
0 ignored issues
show
Unused Code introduced by
The parameter e is not used and could be removed.

This check looks for parameters in functions that are not used in the function body and are not followed by other parameters which are used inside the function.

Loading history...
689
        // Update the order field based on the current rows item length.
690
        populateOrderField();
691
    }
692
693
    function loader(container) {
694
        container.select('.loader-overlay').classed({hidden: false});
695
        container.select('.widget-loader').classed({hidden: false});
696
    }
697
698
    function unload(container) {
699
        container.select('.loader-overlay').classed({hidden: true});
700
        container.select('.widget-loader').classed({hidden: true});
701
    }
702
703
    /**
704
     * [addDomEvents Add all dom event handlers here]
705
     */
706
    function addDomEvents() {
707
        MAIN_FORM.find('[name="mode"]').on('change.charts.row', chartsModeChanged);
708
        WIDGET_FORM.find('[name="row"]').on('change.charts.row', chartsRowChanged);
709
        // Chart type change
710
        WIDGET_FORM.find('[name="type"]').on('change.charts.type', chartsTypeChanged);
711
        // TODO: debounce/throttle
712
        API_PREVIEW_BTN.on('click.charts', previewAPIRoute);
713
        // Save module popup form
714
        SAVE_WIDGET_BTN.on('click.charts.save', saveWidget);
715
        // Edit existing modules
716
        EDIT_MODAL.on('show.bs.modal', populateEditForm);
717
        UPDATE_FORM_BTN.on('click.charts.save', onUpdateWidget);
718
719
        // Allow swapping of edit/update events
720
        // for the add module button and form modal
721
        ADD_MODULE.on('click.charts', function(){
722
            UPDATE_FORM_BTN
723
            .attr('id', SAVE_WIDGET_BTN.selector.replace('#', ''))
724
            .text('Save widget')
725
            .off('click.charts.save')
726
            .on('click.charts.save', saveWidget);
727
        });
728
729
        // Allow swapping of edit/update events
730
        // for the add module per row button and form modal
731
        VIEW_BUILDER.on('click.charts', '.grid-row-label', function(){
732
            UPDATE_FORM_BTN
733
            .attr('id', SAVE_WIDGET_BTN.selector.replace('#', ''))
734
            .text('Save widget')
735
            .off('click.charts.save')
736
            .on('click.charts.save', saveWidget);
737
        });
738
739
        // Add delete button for existing widgets.
740
        DELETE_BTN.on('click.charts', function(e){
741
            e.preventDefault();
742
            var guid = WIDGET_FORM.attr('data-guid');
743
            var widget = my.widgets.get(guid).delete(false);
0 ignored issues
show
Unused Code introduced by
The variable widget seems to be never used. Consider removing it.
Loading history...
744
        });
745
        // Add delete confirm for dashboards.
746
        DELETE_DASHBOARD.on('submit.charts', function(e){
747
            if(!confirm('Are you sure?')) e.preventDefault();
0 ignored issues
show
Coding Style Best Practice introduced by
Curly braces around statements make for more readable code and help prevent bugs when you add further statements.

Consider adding curly braces around all statements when they are executed conditionally. This is optional if there is only one statement, but leaving them out can lead to unexpected behaviour if another statement is added later.

Consider:

if (a > 0)
    b = 42;

If you or someone else later decides to put another statement in, only the first statement will be executed.

if (a > 0)
    console.log("a > 0");
    b = 42;

In this case the statement b = 42 will always be executed, while the logging statement will be executed conditionally.

if (a > 0) {
    console.log("a > 0");
    b = 42;
}

ensures that the proper code will be executed conditionally no matter how many statements are added or removed.

Loading history...
748
        });
749
750
        // Format json config display
751
        $('#json-output').on('show.bs.modal', function(e){
0 ignored issues
show
Unused Code introduced by
The parameter e is not used and could be removed.

This check looks for parameters in functions that are not used in the function body and are not followed by other parameters which are used inside the function.

Loading history...
752
            var code = $(this).find('code').text();
753
            $(this).find('code').text(prettyCode(code));
754
        });
755
756
        // Add event for downloading json config raw.
757
        // Will provide decent support but still not major: http://caniuse.com/#search=download
758
        $('[href="#download-json"]').on('click', function(e){
0 ignored issues
show
Unused Code introduced by
The parameter e is not used and could be removed.

This check looks for parameters in functions that are not used in the function body and are not followed by other parameters which are used inside the function.

Loading history...
759
            var datestr = new Date().toString().replace(/ /gi, '-');
760
            var data = encodeURIComponent(JSON.stringify(JSON_DATA.val(), null, 4));
761
            data = "data:text/json;charset=utf-8," + data;
762
            $(this).attr('href', data);
763
            $(this).attr('download', 'charts-config-raw-' + datestr + '.json');
764
        });
765
766
        // For fixed grid, add events for making new rows.
767
        ADD_ROW_CONTS.find('.btn').on('click', addNewRow);
768
769
        EDIT_TOGGLE_BTN.on('click', function(e){
0 ignored issues
show
Unused Code introduced by
The parameter e is not used and could be removed.

This check looks for parameters in functions that are not used in the function body and are not followed by other parameters which are used inside the function.

Loading history...
770
            $('body').toggleClass('jsondash-editing');
771
            updateRowControls();
772
        });
773
774
        $('.delete-row').on('click', function(e){
775
            e.preventDefault();
776
            var row = $(this).closest('.grid-row');
777
            if(row.find('.item.widget').length > 0) {
778
                if(!confirm('Are you sure?')) {
779
                    return;
780
                }
781
            }
782
            deleteRow(row);
783
        });
784
    }
785
786
    function initFixedDragDrop(options) {
787
        var grid_drag_opts = {
788
            connectToSortable: '.grid-row'
789
        };
790
        $('.grid-row').droppable({
791
            drop: function(event, ui) {
792
                // update the widgets location
793
                var idx    = $(this).index();
794
                var el     = $(ui.draggable);
795
                var widget = my.widgets.getByEl(el);
796
                widget.update({row: idx}, true);
797
                // Actually move the dom element, and reset
798
                // the dragging css so it snaps into the row container
799
                el.parent().appendTo($(this));
800
                el.css({
801
                    position: 'relative',
802
                    top: 0,
803
                    left: 0
804
                });
805
            }
806
        });
807
        $('.item.widget').draggable($.extend(grid_drag_opts, options));
808
    }
809
810
    function fitGrid(grid_packer_opts, init) {
811
        var packer_options = $.isPlainObject(grid_packer_opts) ? grid_packer_opts : {};
812
        var grid_packer_options = $.extend({}, packer_options, {});
813
        var drag_options = {
814
            scroll: true,
815
            handle: '.dragger',
816
            start: function() {
817
                $('.grid-row').addClass('drag-target');
818
            },
819
            stop: function(){
820
                $('.grid-row').removeClass('drag-target');
821
                EDIT_CONTAINER.collapse('show');
822
                if(my.layout === 'grid') {
823
                    // Update row order.
824
                    updateChartsRowOrder();
825
                } else {
826
                    my.chart_wall.packery(grid_packer_options);
827
                    updateChartsOrder();
828
                }
829
            }
830
        };
831
        if(my.layout === 'grid' && $('.grid-row').length > 1) {
832
            initFixedDragDrop(drag_options);
833
            return;
834
        }
835
        if(init) {
836
            my.chart_wall = $('#container').packery(grid_packer_options);
837
            items = my.chart_wall.find('.item').draggable(drag_options);
0 ignored issues
show
Bug introduced by
The variable items seems to be never declared. Assigning variables without defining them first makes them global. If this was intended, consider making it explicit like using window.items.
Loading history...
838
            my.chart_wall.packery('bindUIDraggableEvents', items);
839
        } else {
840
            my.chart_wall.packery(grid_packer_options);
841
        }
842
    }
843
844
    function updateChartsOrder() {
845
        // Update the order and order value of each chart
846
        var items = my.chart_wall.packery('getItemElements');
847
        // Update module order
848
        $.each(items, function(i, el){
0 ignored issues
show
Unused Code introduced by
The parameter el is not used and could be removed.

This check looks for parameters in functions that are not used in the function body and are not followed by other parameters which are used inside the function.

Loading history...
849
            var widget = my.widgets.getByEl($(this));
850
            widget.update({order: i}, true);
851
        });
852
    }
853
854
    function handleInputs(widget, config) {
855
        var inputs_selector = '[data-guid="' + config.guid + '"] .chart-inputs';
856
        // Load event handlers for these newly created forms.
857
        $(inputs_selector).find('form').on('submit', function(e){
858
            e.stopImmediatePropagation();
859
            e.preventDefault();
860
            // Just create a new url for this, but use existing config.
861
            // The global object config will not be altered.
862
            // The first {} here is important, as it enforces a deep copy,
863
            // not a mutation of the original object.
864
            var url = config.dataSource;
865
            // Ensure we don't lose params already save on this endpoint url.
866
            var existing_params = url.split('?')[1];
867
            var params = jsondash.util.getValidParamString($(this).serializeArray());
868
            params = jsondash.util.reformatQueryParams(existing_params, params);
869
            var _config = $.extend({}, config, {
870
                dataSource: url.replace(/\?.+/, '') + '?' + params
871
            });
872
            my.widgets.get(config.guid).update(_config, true);
873
            // Otherwise reload like normal.
874
            my.widgets.get(config.guid).load();
875
            // Hide the form again
876
            $(inputs_selector).removeClass('in');
877
        });
878
    }
879
880
    function getHandler(family) {
881
        var handlers  = {
882
            basic          : jsondash.handlers.handleBasic,
883
            datatable      : jsondash.handlers.handleDataTable,
884
            sparkline      : jsondash.handlers.handleSparkline,
885
            timeline       : jsondash.handlers.handleTimeline,
886
            venn           : jsondash.handlers.handleVenn,
887
            graph          : jsondash.handlers.handleGraph,
888
            wordcloud      : jsondash.handlers.handleWordCloud,
889
            vega           : jsondash.handlers.handleVegaLite,
890
            plotlystandard : jsondash.handlers.handlePlotly,
891
            cytoscape      : jsondash.handlers.handleCytoscape,
892
            sigmajs        : jsondash.handlers.handleSigma,
893
            c3             : jsondash.handlers.handleC3,
894
            d3             : jsondash.handlers.handleD3,
895
            flamegraph     : jsondash.handlers.handleFlameGraph
896
        };
897
        return handlers[family];
898
    }
899
900
    function addResizeEvent(widg) {
901
        // Add resize event
902
        var resize_opts = {
903
            helper: 'resizable-helper',
904
            minWidth: MIN_CHART_SIZE,
905
            minHeight: MIN_CHART_SIZE,
906
            maxWidth: VIEW_BUILDER.width(),
907
            handles: my.layout === 'grid' ? 's' : 'e, s, se',
908
            stop: function(event, ui) {
909
                var newconf = {height: ui.size.height};
910
                if(my.layout !== 'grid') {
911
                    newconf['width'] = ui.size.width;
912
                }
913
                // Update the configs dimensions.
914
                widg.update(newconf);
915
                fitGrid();
916
                // Open save panel
917
                EDIT_CONTAINER.collapse('show');
918
            }
919
        };
920
        // Add snap to grid (vertical only) in fixed grid mode.
921
        // This makes aligning charts easier because the snap points
922
        // are more likely to be consistent.
923
        if(my.layout === 'grid') {resize_opts['grid'] = 20;}
924
        $(widg.el[0]).resizable(resize_opts);
925
    }
926
927
    function prettyCode(code) {
928
        if(typeof code === "object") return JSON.stringify(code, null, 4);
0 ignored issues
show
Coding Style Best Practice introduced by
Curly braces around statements make for more readable code and help prevent bugs when you add further statements.

Consider adding curly braces around all statements when they are executed conditionally. This is optional if there is only one statement, but leaving them out can lead to unexpected behaviour if another statement is added later.

Consider:

if (a > 0)
    b = 42;

If you or someone else later decides to put another statement in, only the first statement will be executed.

if (a > 0)
    console.log("a > 0");
    b = 42;

In this case the statement b = 42 will always be executed, while the logging statement will be executed conditionally.

if (a > 0) {
    console.log("a > 0");
    b = 42;
}

ensures that the proper code will be executed conditionally no matter how many statements are added or removed.

Loading history...
929
        return JSON.stringify(JSON.parse(code), null, 4);
930
    }
931
932
    function prettifyJSONPreview() {
933
        // The raw config is hidden in demo mode,
934
        // so this will throw an error otherwise
935
        if(jsondash.util.isInDemoMode()) {return;}
936
        // Reformat the code inside of the raw json field,
937
        // to pretty print for the user.
938
        JSON_DATA.text(prettyCode(JSON_DATA.text()));
939
    }
940
941
    function addNewRow(e) {
942
        // Add a new row with a toggleable label that indicates
943
        // which row it is for user editing.
944
        var placement = 'top';
945
        if(e) {
946
            e.preventDefault();
947
            placement = $(this).closest('.row').data().rowPlacement;
948
        }
949
        var el = ROW_TEMPLATE.clone(true);
950
        el.removeClass('grid-row-template');
951
        if(placement === 'top') {
952
            VIEW_BUILDER.find('.add-new-row-container:first').after(el);
953
        } else {
954
            VIEW_BUILDER.find('.add-new-row-container:last').before(el);
955
        }
956
        // Update the row ordering text
957
        updateRowOrder();
958
        // Add new events for dragging/dropping
959
        fitGrid();
960
        el.trigger(EVENTS.add_row);
961
    }
962
963
    function updateChartsRowOrder() {
964
        // Update the row order for each chart.
965
        // This is necessary for cases like adding a new row,
966
        // where the order is updated (before or after) the current row.
967
        // NOTE: This function assumes the row order has been recalculated in advance!
968
        $('.grid-row').each(function(i, row){
969
            $(row).find('.item.widget').each(function(j, item){
970
                var widget = my.widgets.getByEl($(item));
971
                widget.update({row: i + 1, order: j + 1}, true);
972
            });
973
        });
974
    }
975
976
    function updateRowOrder() {
977
        $('.grid-row').not('.grid-row-template').each(function(i, row){
978
            var idx = $(row).index();
979
            $(row).find('.grid-row-label').attr('data-row', idx);
980
            $(row).find('.rownum').text(idx);
981
        });
982
        updateChartsRowOrder();
983
    }
984
985
    function loadDashboard(data) {
986
        // Load the grid before rendering the ajax, since the DOM
987
        // is rendered server side.
988
        fitGrid({
989
            columnWidth: 5,
990
            itemSelector: '.item',
991
            transitionDuration: 0,
992
            fitWidth: true
993
        }, true);
994
        $('.item.widget').removeClass('hidden');
995
996
        // Populate widgets with the config data.
997
        my.widgets.populate(data);
998
999
        // Load all widgets, adding actual ajax data.
1000
        my.widgets.loadAll();
1001
1002
        // Setup responsive handlers
1003
        var jres = jRespond([{
1004
            label: 'handheld',
1005
            enter: 0,
1006
            exit: 767
1007
        }]);
1008
        jres.addFunc({
1009
            breakpoint: 'handheld',
1010
            enter: function() {
1011
                $('.widget').css({
1012
                    'max-width': '100%',
1013
                    'width': '100%',
1014
                    'position': 'static'
1015
                });
1016
            }
1017
        });
1018
        prettifyJSONPreview();
1019
        populateRowField();
1020
        fitGrid();
1021
        if(isEmptyDashboard()) {EDIT_TOGGLE_BTN.click();}
1022
        MAIN_CONTAINER.trigger(EVENTS.init);
1023
    }
1024
1025
    /**
1026
     * [updateRowControls Check each row's buttons and disable the "add" button if that row
1027
     * is at the maximum colcount (12)]
1028
     */
1029
    function updateRowControls() {
1030
        $('.grid-row').not('.grid-row-template').each(function(i, row){
1031
            var count = getRowColCount($(row));
1032
            if(count >= 12) {
1033
                $(row).find('.grid-row-label').addClass('disabled');
1034
            } else {
1035
                $(row).find('.grid-row-label').removeClass('disabled');
1036
            }
1037
        });
1038
    }
1039
1040
    /**
1041
     * [getRowColCount Return the column count of a row.]
1042
     * @param  {[dom selection]} row [The row selection]
1043
     */
1044
    function getRowColCount(row) {
1045
        var count = 0;
1046
        row.find('.item.widget').each(function(j, item){
1047
            var classes = $(item).parent().attr('class').split(/\s+/);
1048
            for(var i in classes) {
0 ignored issues
show
Complexity introduced by
A for in loop automatically includes the property of any prototype object, consider checking the key using hasOwnProperty.

When iterating over the keys of an object, this includes not only the keys of the object, but also keys contained in the prototype of that object. It is generally a best practice to check for these keys specifically:

var someObject;
for (var key in someObject) {
    if ( ! someObject.hasOwnProperty(key)) {
        continue; // Skip keys from the prototype.
    }

    doSomethingWith(key);
}
Loading history...
1049
                if(classes[i].startsWith('col-md-')) {
1050
                    count += parseInt(classes[i].replace('col-md-', ''), 10);
0 ignored issues
show
Bug introduced by
The variable count is changed as part of the for-each loop for example by parseInt(classes.i.replace("col-md-", ""), 10) on line 1050. Only the value of the last iteration will be visible in this function if it is called after the loop.
Loading history...
1051
                }
1052
            }
1053
        });
1054
        return count;
1055
    }
1056
1057
    function isEmptyDashboard() {
1058
        return $('.item.widget').length === 0;
1059
    }
1060
1061
    my.config = {
1062
        WIDGET_MARGIN_X: 20,
1063
        WIDGET_MARGIN_Y: 60
1064
    };
1065
    my.loadDashboard = loadDashboard;
1066
    my.handlers = {};
1067
    my.util = {};
1068
    my.loader = loader;
1069
    my.unload = unload;
1070
    my.addDomEvents = addDomEvents;
1071
    my.getActiveConfig = getParsedFormConfig;
1072
    my.layout = VIEW_BUILDER.length > 0 ? VIEW_BUILDER.data().layout : null;
1073
    my.widgets = new Widgets();
1074
    return my;
1075
}();
1076